import math

bin = lambda x,bits: ''.join('1' if x & (1<<i) else '0' for i in range(bits-1,-1,-1))

def modInverse(mult, mod):
	for i in range(mod):
		if (i*mult) % mod == 1:
			return i
	return None

def checkMod(mod, set):
	sum = 0
	for num in set:
		sum += num
		
	if mod > sum:
		return True
		
	return False
	
def checkMult(mult, mod, set):
	#make sure the multiplier has an inverse
	if modInverse(mult, mod) == None:
		return false
		
	new_set = []
	for num in set:
		new_set.append( (num * mult) % mod)
	
	oldSum = 0
	for num in new_set:
		if num < oldSum:
			return True
		else:
			oldSum += num
	
	return False

def createMask(width):
	mask = 1
	for i in range(width - 1):
		mask = (mask << 1) + 1

	return mask

def textToInt(text):
	num = 0
	for char in text:
		num += ord(char)
		num = num << 8	
	num = num >> 8
	return num

#numBits is a multiple of 8
#num is a 'numBits' length number
def intToText(num, numBits):
	mask = 0xFF
	text = []
	numChars = numBits / 8
	
	for i in range(numChars - 1, -1, -1):
		#add the extras to the number of characters to shift off so that the trailing
		#bits that were added during the encryption go away nicely
		text.append(str(chr(mask & (num >> (i*8)))))
	return ''.join(text)
	

def getEncryption(num, set):
	sum = 0
	for i in range(0, len(set)):
		if (num & 2**(i)):
			#need the negative index because i is going from right to left
			#but the set goes from left to right.  So we need to index from
			#the right.  Thus negative array indices
			sum += set[-(i + 1)]
			
	return sum
	

def encrypt(plain, set, mod, mult):
	num = textToInt(plain)	
	numBits = len(plain) * 8

	#figure out how many bits the number needs to be shifted left so that
	#the encryption will have all numbers be the same number of bits
	extraBits = len(set) - (numBits % len(set))
	num = num << extraBits
	numBits += extraBits
	numBlocks = numBits / len(set)
	cipher = []
	
	mask = createMask(len(set))	
	
	for i in range(1, numBlocks + 1):
		#encrypt ith block
		#the mask should be a set of 1's of length len(set) at the same block as we
		#are currently working with. Thus anding them will yield only those 1s in the
		#current block.  We still need to shift it to get the acutal number
		
		temp = mask & (num >> ((numBlocks - i) * len(set)))
		enc = getEncryption(temp, set)
		cipher.append((enc * mult) % mod)
	
	return cipher

##### DECRYPTION IS WORKING NOW ######

def getDecryption(num, set):
	#snap decryption algorithm
	binStr = []
	for i in range(len(set) - 1, -1, -1):
		if set[i] <= num:
			binStr.append('1')
			num -= set[i]
		else:
			binStr.append('0')
	

	binStr.reverse()
	#return the binary string converted to an integer in base 2
	return int(''.join(binStr), 2)

def decrypt(cipher, set, mod, mult, inv):
	plain = 0
	numBits = 0	

	for number in cipher:
		dec = getDecryption((number * inv) % mod ,set)
		plain = (plain + dec) << len(set)
		numBits += len(set)
	
	#have to undo the last shift because there are no more letters to be added
	plain = plain >> len(set)
	
	#shift off the remaining padded bits
	return intToText(plain >> ((len(cipher) * len(set)) % 8), numBits - ((len(cipher) * len(set)) % 8))
	

# -------- File Parsing stuff -------- #
def parseCipherFile(fname):
	numbers = []
	file = open(fname, 'r')
	for line in file:
		numbers.extend(int(i) for i in line.split())
	file.close()
	return numbers
	
def parseVectorFile(fname):
	vector = []
	file = open(fname, 'r')
	for line in file:
		if line != '':
			vector.append(int(line))
	file.close()
	return vector

def parsePlainFile(fname):
	plain = []
	f = open(fname, 'r')
	for line in f:
		plain.append(line)
	f.close()
	return '\n'.join(plain)
	
def writeCipherFile(fname, cipher):
	file = open(fname, 'w')
	file.write(' '.join(str(i) for i in cipher))
	file.write('\n')
	file.close()
	
def writePlainFile(fname, plain):
	file = open(fname, 'w')
	file.write(plain)
	file.close()
	
def writeVectorFile(fname, vec):
	file = open(fname, 'w')
	file.write('\n'.join(str(i) for i in vec))
	file.write('\n')
	file.close()
	
	
